package org.textreport.core;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;


public class DefaultSQLType implements SQLType {
	private ResultSet rset;
	private PreparedStatement pstm;
	private Connection c;
	private Set<String> paramNames = new HashSet<String>();
	private List<String> pIndex = new ArrayList<String>();
	private Map<String, Object> paramValues = new HashMap<String, Object>();
	private String sql;
	private boolean hasLines;
	private ExecutionContext ectx;

	public DefaultSQLType(Connection c) {
		this.c = c;
	}

	@Override
	public void setSql(String sql) {
		this.sql = processParams(sql);
	}

	private String processParams(String sql) {
		Pattern p = Pattern.compile(":(\\w+)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);

		StringBuilder buf = new StringBuilder(sql);

		Matcher m = p.matcher(buf);

		while (m.find()) {
			String name = m.group(1).trim();
			buf.replace(m.start(), m.end(), "?");
			pIndex.add(name);
			paramNames.add(name);
			m.reset(buf);
		}

		return buf.toString();
	}

	@Override
	public SQLType setParam(Object... params) {
		try {
			if(params.length % 2 > 0){
				throw new IllegalStateException("setParam() a quantidade de argumentos deve ser par.");
			}
			
			for(int i = 0; i < params.length/2; i+=2){
				paramValues.put((String) params[i], params[i+1]);
			}
			return this;
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public boolean open() {
		try {
			close();
			pstm = c.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
			injectParams();

			rset = pstm.executeQuery();

			hasLines = rset.next();
			return hasLines;
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	private void injectParams() throws SQLException {
		int i = 1;
		for (String name : pIndex) {
			Object o = paramValues.get(name);
			if (o instanceof Number) {
				pstm.setInt(i++, ((Number) o).intValue());
			} else {
				pstm.setObject(i++, o);
			}
		}
	}

	@Override
	public void close() {
		try {
			if (pstm != null) {
				pstm.close();
				pstm = null;
			}
		} catch (Exception ignored) {
		}
	}

	@Override
	public boolean next() {
		try {
			hasLines = rset.next();
			return hasLines;
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public int integer(String name) {
		try {
			return hasLines ? rset.getInt(name) : 0;
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}

	}

	@Override
	public String string(String name) {
		try {
			return hasLines ? rset.getString(name) : "";
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public Timestamp date(String name) {
		try {
			return hasLines ? rset.getTimestamp(name) : null;
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public double number(String name) {
		try {
			return hasLines ? rset.getDouble(name) : 0d;
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	@Override
	public Set<String> extractParams() {
		return paramNames;
	}

	@Override
	public SQLType exec() {
		SQLUtils.sqlOpen(this, ectx);

		return this;
	}

	@Override
	public void setContext(ExecutionContext ctx) {
		ectx = ctx;
	}

	@Override
	public Node xml(String name) {
		if (hasLines) {
			try {
				Clob data = rset.getClob(name);

				DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
				DocumentBuilder builder = factory.newDocumentBuilder();
				InputStream in = data.getAsciiStream();

				Document doc = builder.parse(in);
				in.close();
				return doc;
			} catch (Exception e) {
				throw new IllegalStateException(e);
			}
		}

		return null;
	}

	@Override
	public Map<String, Object> getFieldValues() {
		try {
			ResultSetMetaData rsmd = rset.getMetaData();
			Map<String, Object> result = new HashMap<String, Object>();

			for (int i = 1; i <= rsmd.getColumnCount(); i++) {
				String name = rsmd.getColumnLabel(i);
				int type = rsmd.getColumnType(i);

				switch (type) {
					case Types.DECIMAL:
					case Types.FLOAT:
					case Types.DOUBLE:
					case Types.NUMERIC:
					case Types.REAL:
						result.put(name, rset.getDouble(i));
						break;
					case Types.INTEGER:
					case Types.SMALLINT:
					case Types.TINYINT:
						result.put(name, rset.getInt(i));
						break;
					case Types.DATE:
					case Types.TIMESTAMP:
						result.put(name, rset.getTimestamp(i));
						break;
					case Types.CLOB:
						result.put(name, readCLob(rset.getClob(i)));

						break;
					case Types.VARCHAR:
					case Types.NVARCHAR:
					case Types.CHAR:
					case Types.LONGNVARCHAR:
					case Types.LONGVARCHAR:
					case Types.NCHAR:
						result.put(name, rset.getString(i));
						break;

					default:
						result.put(name, rset.getObject(i));
						break;
				}
			}

			return result;
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	private String readCLob(Clob clob) throws SQLException, IOException {
		StringBuilder buf = new StringBuilder();

		BufferedReader br = new BufferedReader(clob.getCharacterStream());
		char[] cb = new char[512];
		int readen = 0;
		while ((readen = br.read(cb)) > -1) {
			buf.append(cb, 0, readen);
		}

		return buf.toString();
	}

}